//blockchain.go

package blockchain
import (
	"bytes"
	"encoding/hex"
	"fmt"
	"goblockchain/constcoe"
	"goblockchain/transaction"
	"goblockchain/utils"
	"runtime"

	// "github.com/dgraph-io/badger"
	// new version
	badger "github.com/dgraph-io/badger/v4"
)

// blockchain euqals linkedlist of blocks
type BlockChain struct {
	// 根据最新的区块就可以一直往前找到所有的区块，可以看成是传统链表的链表头结点
	LastHash []byte
	Database *badger.DB
}

//	func CreateBlockChain() *BlockChain {
//		blockchain := BlockChain{}
//		blockchain.Blocks = append(blockchain.Blocks, GenesisBlock([]byte("alan")))
//		return &blockchain
//	}

// init a new blockchain
func InitBlockChain(address []byte) *BlockChain {
	var lastHash []byte

	// check existance
	if utils.FileExists(constcoe.BCFile) {
		fmt.Println("BlockChain already exists")
		runtime.Goexit()
	}

	// db config
	opts := badger.DefaultOptions(constcoe.BCPath)
	opts.Logger = nil

	// open a connection pool
	db, err := badger.Open(opts)
	utils.Handle(err)

	// create GenesisBlock and store in db
	err = db.Update(func(txn *badger.Txn) error {
		genesis := GenesisBlock(address)
		fmt.Println("Genesis created")
		err = txn.Set(genesis.Hash, genesis.Serialize())
		utils.Handle(err)
		err = txn.Set([]byte("lh"), genesis.Hash)
		utils.Handle(err)
		err = txn.Set([]byte("ogprevhash"), genesis.Hash)
		utils.Handle(err)
		lastHash = genesis.Hash
		return err
	})
	utils.Handle(err)

	// struct a blockchain
	blockchain := &BlockChain{
		LastHash: lastHash,
		Database: db,
	}
	return blockchain
}

// read blockchain from db
func ContinueBlockChain() *BlockChain {
	if !utils.FileExists(constcoe.BCFile) {
		fmt.Println("No blockchain found, please create one first")
		runtime.Goexit()
	}
	var lastHash []byte
	opst := badger.DefaultOptions(constcoe.BCPath)
	opst.Logger = nil
	db, err := badger.Open(opst)
	utils.Handle(err)

	err = db.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("lh"))
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			lastHash = val
			return err
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)

	blockchain := &BlockChain{
		LastHash: lastHash,
		Database: db,
	}
	return blockchain
}

// add new block into blockchain and store (after mining)
func (bc *BlockChain) AddBlock(newBlock *Block) {

	// 1. 读取链上的最新的一块
	var lastHash []byte
	err := bc.Database.View(func(txn *badger.Txn) error {

		item, err := txn.Get([]byte("lh"))
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			lastHash = val
			return err
		})
		return err
	})
	utils.Handle(err)

	// 2. 检查新构造的区块的上一块是不是链上的最新的一块
	if !bytes.Equal(newBlock.PrevHash, lastHash) {
		fmt.Println("This block is out of age")
		runtime.Goexit()
	}

	// 3. 把新区块链接到区块链上，并存储起来(直接把新区块存进去，更新最新区块标记即可，达成隐式链接)
	err = bc.Database.Update(func(txn *badger.Txn) error {
		err := txn.Set(newBlock.Hash, newBlock.Serialize())
		utils.Handle(err)
		err = txn.Set([]byte("lh"), newBlock.Hash)
		bc.LastHash = newBlock.Hash
		return err
	})
	utils.Handle(err)
}

// 由于加入存储机制，对于区块链的遍历变得没有这么方便，所以设计一个迭代器
// 本体就是一个节点指针，不断先前遍历读取区块信息（在数据库中）
type BlockChainIterator struct {
	CurrentHash []byte
	Database    *badger.DB
}
// constructor
func (bc *BlockChain) Iterator() *BlockChainIterator {
	iter := &BlockChainIterator{
		CurrentHash: bc.LastHash,
		Database:    bc.Database,
	}
	return iter
}
// return current block pointer and move to next one
func (iter *BlockChainIterator) Next() *Block {
	var block *Block
	err := iter.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get(iter.CurrentHash)
		utils.Handle(err)
		err = item.Value(func(val []byte) error {
			block = DeSerialize(val)
			return err
		})
		utils.Handle(err)
		return err
	})
	utils.Handle(err)

	iter.CurrentHash = block.PrevHash
	return block
}
// return GenesisBlock hash(stored in db as ogprevhash) 
func (chain *BlockChain) BackOgPrevHash() []byte {
	var ogprevhash []byte
	err := chain.Database.View(func(txn *badger.Txn) error {
		item, err := txn.Get([]byte("ogprevhash"))
		utils.Handle(err)

		err = item.Value(func(val []byte) error {
			ogprevhash = val
			return nil
		})

		utils.Handle(err)
		return err
	})
	utils.Handle(err)
	return ogprevhash
}

// get user's all unspent txs（交易图回溯算法）
// use iterator
func (bc *BlockChain) FindUnspentTransactions(from []byte) []transaction.Transaction {
	var unSpentTxs []transaction.Transaction // 用于记录当前用户的未使用交易切片
	spentTxs := make(map[string][]int)       // 用于标记交易的输出已经被使用（仅仅标记已经被使用），交易ID => {输出的某个索引}

	iter := bc.Iterator()
	OgHash := bc.BackOgPrevHash()

all:
	for {
		block := iter.Next()
		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)
		IterOutputs:
			for outIdx, out := range tx.Outputs { // 检查每一个交易的输出（目标：将没有使用过的加入切片）
				if spentTxs[txID] != nil { // 检查已经使用的输出的前提是，存在已经使用的输出，否则直接到下一个if
					for _, spentOut := range spentTxs[txID] { // 检查当前交易中的每一个已经使用过的并标记过的输出（索引）
						if spentOut == outIdx { // 恰好为当前输出
							continue IterOutputs // 则不用添加，直接检查下一个输出即可，label语法用于跳出\跳过多层循环
						}
					}
				}
				if out.ToAddressRight(from) { // 检查是否是输出到当前用户，否则和当前用户无关
					unSpentTxs = append(unSpentTxs, *tx) // 添加到当前用户的未使用交易的切片中
				}
			}
			if !tx.IsBase() {
				for _, in := range tx.Inputs { // 检查每一个交易的输入（目标：将每一个源输出标记为已经使用过）
					if in.FromAddressRight(from) { // 前提是当前用户的源输出，否则和当前用户无关
						inTxID := hex.EncodeToString(in.TxId)
						spentTxs[inTxID] = append(spentTxs[inTxID], in.OutIdx) // 将过去那个输出标记为已经使用
					}
				}
			}
		}
		if bytes.Equal(block.Hash, OgHash) {
			break all
		}
	}
	return unSpentTxs
}

// get user's all unspent-outputs(UTXOs)
func (bc *BlockChain) FindUTXOs(address []byte) (int, map[string]int) {
	unspentOuts := make(map[string]int)               // 当前用户所有未使用的输出（交易ID+输出索引 确定一个输出）
	unspentTxs := bc.FindUnspentTransactions(address) // 当前用户所有未使用的交易
	accumulated := 0
Work:
	for _, tx := range unspentTxs {
		txID := hex.EncodeToString(tx.ID)
		for index, out := range tx.Outputs {
			if out.ToAddressRight(address) {
				accumulated += out.Value
				unspentOuts[txID] = index
				// one transaction can only have one output referred to adderss
				// so rediect to next tx ,then check its outputs
				// use lable to cross serveral for
				continue Work
			}
		}
	}
	return accumulated, unspentOuts
}

// get user's target unspent-outputs(UTXOs) for a tx-amount
func (bc *BlockChain) FindSpendableOutputs(address []byte, amount int) (int, map[string]int) {
	unspentOuts := make(map[string]int)
	unspentTxs := bc.FindUnspentTransactions(address)
	accumulated := 0
Work:
	for _, tx := range unspentTxs {
		txID := hex.EncodeToString(tx.ID)
		for index, out := range tx.Outputs {
			if out.ToAddressRight(address) {
				accumulated += out.Value
				unspentOuts[txID] = index
				// enough
				if accumulated >= amount {
					break Work
				}
				continue Work
			}
		}
	}
	return accumulated, unspentOuts
}

// create a new tx
func (bc *BlockChain) CreateTransaction(from, to []byte, amount int) (*transaction.Transaction, bool) {
	// 直观上的一笔交易构成：发送者、接收者、数量
	// 实际UTXO系统内部的数据结构构成：交易哈希（ID）、若干输入（TxInput）、若干输出（TxOutput）

	txInputs := make([]transaction.TxInput, 0)
	txOutputs := make([]transaction.TxOutput, 0)
	// 1. build TxInputs: inputs come from outputs(UTXOs)
	total, unspentOuts := bc.FindSpendableOutputs(from, amount)
	if total < amount {
		fmt.Println("Not enough coins!")
		// return nil,false (this is better actually)
		return &transaction.Transaction{}, false
	}
	for txId, outIndex := range unspentOuts {
		id, err := hex.DecodeString(txId)
		utils.Handle(err)
		txInput := transaction.TxInput{
			TxId:        id,
			OutIdx:      outIndex,
			FromAddress: from,
		}
		txInputs = append(txInputs, txInput)
	}
	// 2. build TxOutputs: output can divde into change-back and sent-amount
	txOutputs = append(txOutputs, transaction.TxOutput{
		Value:     amount,
		ToAddress: to,
	})
	if total > amount {
		txOutputs = append(txOutputs, transaction.TxOutput{
			Value:     total - amount,
			ToAddress: from,
		})
	}
	// 3. set hash
	tx := transaction.Transaction{
		// ID: []byte{},
		ID:      nil,
		Inputs:  txInputs,
		Outputs: txOutputs,
	}
	tx.SetID()
	return &tx, true
}